

namespace Stee.CAP8.MessageBus.SampleUI.ViewModels
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using Microsoft.Extensions.DependencyInjection;
    using NLog;
    using Stee.CAP8.MessageBus.Interfaces.Enums;
    using Stee.CAP8.MessageBus.Interfaces.Interfaces;
    using Stee.CAP8.MessageBus.Interfaces.Models;
    using Stee.CAP8.MessageBus.Interfaces.Utility;
    using Stee.CAP8.MessageBus.Kafka.Admin;
    using Stee.CAP8.MessageBus.Kafka.Configurations;
    using Stee.CAP8.MessageBus.Kafka.Constants;
    using Stee.CAP8.MessageBus.Kafka.Publisher;
    using Stee.CAP8.MessageBus.Kafka.Subscriber;
    using Stee.CAP8.MessageBus.Kafka.Utility;
    using Stee.CAP8.MessageBus.SampleUI.Models;

    /// <summary>
    /// Sample features
    /// - Publish
    /// - Subscribe
    /// - Create topics (including DLQs)
    /// - Check expired messages
    /// </summary>
    public class MessageViewModel : INotifyPropertyChanged
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();


        private const string BrokerConfigurationJSON = "connectionConfigs.json";

        private const string TopicDLQConfigurationJSON = "topicDeadLetterConfigs.json";

        private const string TopicConfigurationJSON = "topicConfigs.json";

        private static readonly TimeSpan ExpiryTimespan = TimeSpan.FromMinutes(2);


        private const string FirstConsumerGroup = "first_consumer_group";

        private const string SecondConsumerGroup = "second_consumer_group";


        #region Topic Information

        private const string Topic = "first_topic";
        private const string Key = "first_key";

        private const string Topic2 = "second_topic";
        private const string Key2 = "second_key";

        private const string Topic3 = "third_topic";
        private const string Key3 = "third_key";

        private const string Topic4 = "fourth_topic";
        private const string Key4 = "fourth_key";

        private const string Topic5 = "fifth_topic";
        private const string Key5 = "fifth_key";

        private readonly TopicName topicNameString = new TopicName(Topic, Key, ExpiryTimespan);
        private readonly TopicName topicNameObject = new TopicName(Topic2, Key2, ExpiryTimespan);
        private readonly TopicName topicNameCommand = new TopicName(Topic3, Key3, ExpiryTimespan);
        private readonly TopicName topicNameEvent = new TopicName(Topic4, Key4, ExpiryTimespan);
        private readonly TopicName topicNameDLQ = new TopicName(Topic5, Key5, ExpiryTimespan);

        #endregion Topic Information


        #region Admin Client

        private readonly IAdmin adminClient;

        #endregion Admin Client


        #region Publisher & Subscriber

        private IPublisher stringPublisher;
        private IPublisher objectPublisher;
        private IPublisher commandPublisher;
        private IPublisher eventPublisher;
        private CancellationTokenSource stringPublishToken;
        private CancellationTokenSource objectPublishToken;
        private CancellationTokenSource commandPublishToken;
        private CancellationTokenSource eventPublishToken;

        private ISubscriber stringSubscriber;
        private ISubscriber objectSubscriber;
        private ISubscriber commandSubscriber;
        private ISubscriber eventSubscriber;
        private ISubscriber dlqSubscriber;

        #endregion Publisher & Subscriber


        #region Service Provider & Others

        private ServiceProvider serviceProvider;

        #endregion Service Provider & Others


        private bool isDLQStarted = false;

        private string textBlockResult;

        /// <summary>
        /// Gets or sets the text block string
        /// </summary>
        public string TextBlockResult
        {
            get { return this.textBlockResult; }
            set
            {
                this.textBlockResult = value;
                this.NotifyPropertyChanged(nameof(this.TextBlockResult));
            }
        }


        #region Constructor & Initializers

        /// <summary>
        /// Initializes a new instance of the MessageViewModel class
        /// </summary>
        public MessageViewModel()
        {
            this.TextBlockResult = string.Empty;
            this.InitializeServiceProvider();

            this.adminClient = this.serviceProvider.GetService<IAdmin>();
            this.adminClient.Initialize(BrokerConfigurationJSON, this.OnAdminClientError);


            var stopwatch = Stopwatch.StartNew();

            this.CreateTopics();
            this.CreateSubscription();

            stopwatch.Stop();
            Console.WriteLine($"[Info] Elapsed time to create topics and subscribe: {stopwatch.Elapsed}");
        }

        ~MessageViewModel()
        {
            this.adminClient.Dispose();

            this.stringSubscriber?.Dispose();
            this.objectSubscriber?.Dispose();
            this.commandSubscriber?.Dispose();
            this.eventSubscriber?.Dispose();
            this.dlqSubscriber?.Dispose();

            this.stringPublisher?.Dispose();
            this.objectPublisher?.Dispose();
            this.commandPublisher?.Dispose();
            this.eventPublisher?.Dispose();
        }

        /// <summary>
        /// Initializes the service provider in creating publisher & subscriber
        /// </summary>
        private void InitializeServiceProvider()
        {
            IServiceCollection serviceCollection = new ServiceCollection();

            //// Add dead-letter queue implementation
            serviceCollection.AddTransient<IDeadLetterQueue, DeadLetterQueueImpl>();

            //// Add admin client implementation
            serviceCollection.AddTransient<IAdmin, AdminClientImpl>();

            //// Add publisher implementation
            serviceCollection.AddTransient<IPublisher, PublisherImpl>();

            //// Add subscriber implementation
            serviceCollection.AddTransient<ISubscriber, SubscriberImpl>();

            //// Create service provider
            this.serviceProvider = serviceCollection.BuildServiceProvider();
        }

        /// <summary>
        /// Create topics and DLQ equivalent
        /// </summary>
        private void CreateTopics()
        {
            try
            {
                if (this.adminClient.IsInitialized)
                {
                    //// Read topic configuration
                    var topicConfigs = ConfigurationHelper.ReadMultiTopicConfiguration(TopicConfigurationJSON, ConfigurationConstants.TopicConfigSection);

                    //// Read DLQ topic configurations (dead-letter queue)
                    var dlqTopicConfigs = ConfigurationHelper.ReadTopicConfiguration(TopicDLQConfigurationJSON, ConfigurationConstants.TopicDLQConfigSection);

                    if (topicConfigs?.Any() != true)
                    {
                        throw new ArgumentNullException($"No topic configuration found. Path: {TopicConfigurationJSON}");
                    }
                    else if (dlqTopicConfigs?.Any() != true)
                    {
                        throw new ArgumentNullException($"No DLQ topic configuration found. Path: {TopicDLQConfigurationJSON}");
                    }

                    //// Create topic
                    foreach (var topicCfg in topicConfigs)
                    {
                        var taskDescTopic =
                            this.adminClient.DescribeTopicAsync(topicCfg[TopicConfiguration.TopicConfigName]);
                        taskDescTopic.Wait();
                        var desc = taskDescTopic.Result;

                        //// Create if topic is not found
                        if (desc == null)
                        {
                            var taskCreateTopic = this.adminClient.CreateNewTopic(topicCfg, dlqTopicConfigs);
                            taskCreateTopic.Wait();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
            }
        }

        /// <summary>
        /// Create subscriptions
        /// </summary>
        private void CreateSubscription()
        {
            this.SubscribeMessageString();
            this.SubscribeMessageObject();
            this.SubscribeMessageCommand();
            this.SubscribeMessageEvent();
        }

        #endregion Constructor & Initializers


        #region Public Methods

        /// <summary>
        /// Publish new messages
        /// </summary>
        public void PublishNewMessages()
        {
            try
            {
                this.TextBlockResult = $"{this.TextBlockResult}=========={Environment.NewLine}";

                this.PublishMessageTypeString();
                this.PublishMessageTypeObject();
                this.PublishCommandMessage();
                this.PublishEventMessage();
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
            }
        }

        /// <summary>
        /// Publish DLQ messages to track expired messages
        /// </summary>
        public void PublishDLQMessages()
        {
            try
            {
                this.TextBlockResult = $"{this.TextBlockResult}=========={Environment.NewLine}";

                using (var publisher = this.serviceProvider.GetService<IPublisher>())
                {
                    publisher.Initialize(BrokerConfigurationJSON, this.OnMessagePublished, this.OnPublisherError);

                    if (publisher.IsInitialized)
                    {
                        var rdmString = RandomString(5);
                        var cancelToken = new CancellationTokenSource(TimeSpan.FromSeconds(30));

                        var message = new GenericMessage
                        {
                            CreateDT = DateTime.Now,
                            Data = JsonSerializer.Serialize(rdmString),
                            MessageDataType = rdmString.GetType().FullName,
                        };
                        message.AddHeader("Name", "SomeName");
                        message.AddHeader("Object1", JsonSerializer.SerializeString(DateTime.Now));
                        message.AddHeader("Object2", JsonSerializer.SerializeString(Guid.NewGuid()));

                        if (publisher.PublishGeneric(this.topicNameDLQ, message, cancelToken.Token))
                        {
                            var msg = $"Publish DLQ string => {rdmString}";
                            this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                            Console.WriteLine(msg);
                        }
                        else
                        {
                            var msg = $"[Error] Publish DLQ string failed => {rdmString}";
                            this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                            Console.WriteLine(msg);
                        }

                        cancelToken.Cancel();
                        cancelToken.Dispose();
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
            }
        }

        /// <summary>
        /// Toggle DLQ topic subscription
        /// </summary>
        public void StartStopDLQSubscription()
        {
            if (this.isDLQStarted)
            {
                this.isDLQStarted = !this.StopDLQSubscription();
            }
            else
            {
                this.isDLQStarted = this.StartDLQSubscription();
            }
        }

        /// <summary>
        /// Check for expired messages
        /// Test setup
        /// *) There are 2 topic types: operational & DLQ
        /// 1) Increase config, "retention.ms", for operational topics (Hard limit retention), where messages
        ///    are deleted from operational topics
        /// 2) Increase config, "retention.ms", for DLQ topics (up to deployment requirement)
        /// 3) Initialize DLQ message check with external retention time (Soft limit retention), where expired messages
        ///    are moved from operational to DLQ topics
        /// 4) Create a consumer for a topic
        /// 5) Without consuming, publish several messages and allow to expire
        /// 6) Start expire check
        /// </summary>
        public void CheckExpiredMessages()
        {
            using (var deadLetterImpl = this.serviceProvider.GetService<IDeadLetterQueue>())
            {
                deadLetterImpl.Initialize(BrokerConfigurationJSON, TimeSpan.FromMinutes(1),
                    OnAdminClientError, OnPublisherError, OnSubscriberError);

                if (deadLetterImpl.IsInitialized)
                {
                    //// This map will be populated with all topics & groups on actual deployment
                    var topicSubscribeGroups = new Dictionary<string, HashSet<string>>
                    {
                        [this.topicNameDLQ.Name] = new HashSet<string>
                        {
                            FirstConsumerGroup,
                            SecondConsumerGroup,
                        },
                    };

                    deadLetterImpl.CheckForExpiredMessages(topicSubscribeGroups);
                }

                deadLetterImpl.CancelCheck();
            }
        }

        /// <summary>
        /// Clear messages
        /// </summary>
        public void ClearScreen()
        {
            try
            {
                this.TextBlockResult = string.Empty;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
            }
        }

        #endregion Public Methods


        /// <summary>
        /// Publish error handler for broker publish error.
        /// </summary>
        /// <param name="errorCode">Publish error code</param>
        /// <param name="errorReason">Error reason in clear text</param>
        /// <param name="isFatal">Flag indicating fatal error</param>
        private void OnAdminClientError(string errorCode, string errorReason, bool isFatal)
        {
            if (string.IsNullOrEmpty(errorCode))
            {
                Logger.Warn($"Unexpected publisher error, no error code returned. Is fatal: {isFatal}");
            }
            else
            {
                Logger.Error($"Publisher error: {errorCode}, Is fatal: {isFatal}");
            }
        }


        #region Publish

        /// <summary>
        /// Publish messages of string data type
        /// </summary>
        private void PublishMessageTypeString()
        {
            this.stringPublisher = this.serviceProvider.GetService<IPublisher>();
            this.stringPublisher.Initialize(BrokerConfigurationJSON, this.OnMessagePublished, this.OnPublisherError);

            if (this.stringPublisher.IsInitialized)
            {
                var rdmString = RandomString(5);
                this.stringPublishToken = new CancellationTokenSource(TimeSpan.FromSeconds(30));

                var message = new GenericMessage
                {
                    CreateDT = DateTime.Now,
                    Data = JsonSerializer.Serialize(rdmString),
                    MessageDataType = typeof(string).FullName,
                };
                message.AddHeader("Name", "SomeName");
                message.AddHeader("Object1", JsonSerializer.SerializeString(DateTime.Now));
                message.AddHeader("Object2", JsonSerializer.SerializeString(Guid.NewGuid()));

                if (this.stringPublisher.PublishGeneric(this.topicNameString, message, this.stringPublishToken.Token))
                {
                    var msg = $"Publish string => {message}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
                else
                {
                    var msg = $"[Error] Publish string failed => {message}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
            }

            this.stringPublishToken.Cancel();
        }

        /// <summary>
        /// Publish messages of object data type
        /// </summary>
        private void PublishMessageTypeObject()
        {
            this.objectPublisher = this.serviceProvider.GetService<IPublisher>();
            this.objectPublisher.Initialize(BrokerConfigurationJSON, this.OnPersonPublished, this.OnPublisherError);

            if (this.objectPublisher.IsInitialized)
            {
                var person = new Person(RandomString(5), RandomNumber(20, 50));
                this.objectPublishToken = new CancellationTokenSource(TimeSpan.FromSeconds(30));

                var message = new GenericMessage
                {
                    CreateDT = DateTime.Now,
                    Data = JsonSerializer.Serialize(person),
                    MessageDataType = typeof(Person).FullName,
                };
                message.AddHeader("Name", "SomeName");
                message.AddHeader("Object1", JsonSerializer.SerializeString(DateTime.Now));
                message.AddHeader("Object2", JsonSerializer.SerializeString(Guid.NewGuid()));

                if (this.objectPublisher.PublishGeneric(this.topicNameObject, message, this.objectPublishToken.Token))
                {
                    var msg = $"Publish Person object => {person}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
                else
                {
                    var msg = $"[Error] Publish Person object failed => {person}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
            }

            this.objectPublishToken.Cancel();
        }

        /// <summary>
        /// Publish command message
        /// </summary>
        private async void PublishCommandMessage()
        {
            this.commandPublisher = this.serviceProvider.GetService<IPublisher>();
            this.commandPublisher.Initialize(BrokerConfigurationJSON, this.OnCommandPublished, this.OnPublisherError);

            if (this.commandPublisher.IsInitialized)
            {
                var commandMsg = new CommandMessage
                {
                    ReplyChannel = "MyChannel",
                    ClientID = "MyClientID",
                    CorrelationID = "MyCorrelationID",
                    CreateDT = DateTime.Now,
                    Data = JsonSerializer.Serialize("MyData"),
                    JWT = "MyJWT",
                    RecordID = "MyRecordID",
                    Action = "MyReplyTopic",
                    Source = "MySource",
                };
                commandMsg.AddHeader("KeyCommand", "ValueCommand");

                this.commandPublishToken = new CancellationTokenSource(TimeSpan.FromSeconds(30));

                if (await this.commandPublisher.PublishCommandAsync(this.topicNameCommand, commandMsg, this.commandPublishToken.Token))
                {
                    var msg = $"Publish CommandMessage object => {commandMsg}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
                else
                {
                    var msg = $"[Error] Publish CommandMessage object failed => {commandMsg}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
            }

            this.commandPublishToken.Cancel();
        }

        /// <summary>
        /// Publish event message
        /// </summary>
        private async void PublishEventMessage()
        {
            this.eventPublisher = this.serviceProvider.GetService<IPublisher>();
            this.eventPublisher.Initialize(BrokerConfigurationJSON, this.OnEventPublished, this.OnPublisherError);

            if (this.eventPublisher.IsInitialized)
            {
                var eventMsg = new EventMessage
                {
                    ClientID = "MyClientID",
                    CorrelationID = "MyCorrelationID",
                    CreateDT = DateTime.Now,
                    Data = JsonSerializer.Serialize("MyData"),
                    JWT = "MyJWT",
                    Action = "MyReplyTopic",
                    Source = "MySource",
                };
                eventMsg.AddHeader("KeyEvent", "ValueEvent");

                this.eventPublishToken = new CancellationTokenSource(TimeSpan.FromSeconds(30));

                if (await this.eventPublisher.PublishEventAsync(this.topicNameEvent, eventMsg, this.eventPublishToken.Token))
                {
                    var msg = $"Publish EventMessage object => {eventMsg}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
                else
                {
                    var msg = $"[Error] Publish EventMessage object failed => {eventMsg}";
                    this.TextBlockResult = $"{this.TextBlockResult}{msg}{Environment.NewLine}";
                    Console.WriteLine(msg);
                }
            }

            this.eventPublishToken.Cancel();
        }

        #endregion Publish


        #region Publish Handlers

        /// <summary>
        /// Publish response handler for string data type messages.
        /// </summary>
        /// <param name="publishOutcome">Message publish outcome</param>
        /// <returns>Return TRUE if handled successfully else FALSE</returns>
        private bool OnMessagePublished(PublishOutcome publishOutcome)
        {
            var isHandled = false;

            if (publishOutcome != null)
            {
                //// Handle your publish outcome here


                isHandled = true;
            }

            return isHandled;
        }

        /// <summary>
        /// Publish response handler for Person data type messages.
        /// </summary>
        /// <param name="publishOutcome">Message publish outcome</param>
        /// <returns>Return TRUE if handled successfully else FALSE</returns>
        private bool OnPersonPublished(PublishOutcome publishOutcome)
        {
            var isHandled = false;

            if (publishOutcome != null)
            {
                //// Handle your publish outcome here


                isHandled = true;
            }

            return isHandled;
        }

        /// <summary>
        /// Publish response handler for command data type messages.
        /// </summary>
        /// <param name="publishOutcome">Message publish outcome</param>
        /// <returns>Return TRUE if handled successfully else FALSE</returns>
        private bool OnCommandPublished(PublishOutcome publishOutcome)
        {
            var isHandled = false;

            if (publishOutcome != null)
            {
                //// Handle your publish outcome here


                isHandled = true;
            }

            return isHandled;
        }

        /// <summary>
        /// Publish response handler for event data type messages.
        /// </summary>
        /// <param name="publishOutcome">Message publish outcome</param>
        /// <returns>Return TRUE if handled successfully else FALSE</returns>
        private bool OnEventPublished(PublishOutcome publishOutcome)
        {
            var isHandled = false;

            if (publishOutcome != null)
            {
                //// Handle your publish outcome here


                isHandled = true;
            }

            return isHandled;
        }

        /// <summary>
        /// Publish error handler for broker publish error.
        /// </summary>
        /// <param name="errorCode">Publish error code</param>
        /// <param name="errorReason">Error reason in clear text</param>
        /// <param name="isFatal">Flag indicating fatal error</param>
        private void OnPublisherError(string errorCode, string errorReason, bool isFatal)
        {
            if (string.IsNullOrEmpty(errorCode))
            {
                Logger.Warn($"Unexpected publisher error, no error code returned. Is fatal: {isFatal}");
            }
            else
            {
                Logger.Error($"Publisher error: {errorCode}, Is fatal: {isFatal}");
            }
        }

        #endregion Publish Handlers


        #region Subscribe

        /// <summary>
        /// Subscribe for messages of string data type
        /// </summary>
        private void SubscribeMessageString()
        {
            this.stringSubscriber = this.serviceProvider.GetService<ISubscriber>();
            this.stringSubscriber.InitializeGeneric(BrokerConfigurationJSON, FirstConsumerGroup, this.topicNameString, this.OnSubscriberError);

            if (this.stringSubscriber.IsInitialized)
            {
                this.stringSubscriber.Subscribe(this.OnMessageReceived);
            }
        }

        /// <summary>
        /// Subscribe for messages of object data type
        /// </summary>
        private void SubscribeMessageObject()
        {
            this.objectSubscriber = this.serviceProvider.GetService<ISubscriber>();
            this.objectSubscriber.InitializeGeneric(BrokerConfigurationJSON, FirstConsumerGroup, this.topicNameObject, this.OnSubscriberError);

            if (this.objectSubscriber.IsInitialized)
            {
                this.objectSubscriber.Subscribe(this.OnPersonReceived);
            }
        }

        /// <summary>
        /// Subscribe for messages of command message data type
        /// </summary>
        private void SubscribeMessageCommand()
        {
            this.commandSubscriber = this.serviceProvider.GetService<ISubscriber>();
            this.commandSubscriber.InitializeCommand(BrokerConfigurationJSON, FirstConsumerGroup, this.topicNameCommand, this.OnSubscriberError);

            if (this.commandSubscriber.IsInitialized)
            {
                this.commandSubscriber.Subscribe(this.OnCommandReceived);
            }
        }

        /// <summary>
        /// Subscribe for messages of event message data type
        /// </summary>
        private void SubscribeMessageEvent()
        {
            this.eventSubscriber = this.serviceProvider.GetService<ISubscriber>();
            this.eventSubscriber.InitializeEvent(BrokerConfigurationJSON, FirstConsumerGroup, this.topicNameEvent, this.OnSubscriberError);

            if (this.eventSubscriber.IsInitialized)
            {
                this.eventSubscriber.Subscribe(this.OnEventReceived, PriorityLevel.Normal, false);
            }
        }


        /// <summary>
        /// Create DLQ subscriber without starting subscription
        /// </summary>
        private bool StartDLQSubscription()
        {
            this.dlqSubscriber = this.serviceProvider.GetService<ISubscriber>();
            this.dlqSubscriber.InitializeGeneric(BrokerConfigurationJSON, FirstConsumerGroup, this.topicNameDLQ, this.OnSubscriberError);

            if (this.dlqSubscriber.IsInitialized)
            {
                this.dlqSubscriber.Subscribe(this.OnMessageReceived);
                return true;
            }
            else
            {
                Logger.Warn("DLQ subscriber is not initialized, unable to subscribe.");
                return false;
            }
        }

        /// <summary>
        /// Start DLQ subscription
        /// </summary>
        private bool StopDLQSubscription()
        {
            if (this.dlqSubscriber != null)
            {
                this.dlqSubscriber.Dispose();
                this.dlqSubscriber = null;
            }

            return true;
        }

        #endregion Subscribe


        #region Subscribe Handlers

        /// <summary>
        /// Handle message of string data type
        /// </summary>
        /// <param name="topicName">Topic name</param>
        /// <param name="messageType">Consumed message type</param>
        /// <param name="message">Consumed topic message</param>
        /// <param name="properties">Message meta-data/properties</param>
        /// <paramref name="publishUtcDT">Published UTC date time</param>
        /// <returns>Return TRUE if consumed successfully else FALSE</returns>
        private bool OnMessageReceived(TopicName topicName, GenericMessage message, DateTime publishUtcDT)
        {
            try
            {
                if (topicName == null)
                {
                    throw new ArgumentNullException(nameof(topicName));
                }

                //TopicName topicName, string messageType, byte[] message, IDictionary<string, byte[]> properties, DateTime publishUtcDT

                //// Handle / process messages here
                //// Consume message data type, string
                var originalMessage = JsonSerializer.Deserialize<string>(message.Data);
                var output = $"Consumed message <string>: {originalMessage}, Properties: ";

                var properties = message.GetAllHeaders();
                output += $"{properties["Name"]}, ";
                var dtStr = properties["Object1"];
                output += $"{JsonSerializer.DeserializeString<DateTime>(dtStr)}, ";
                var guid = properties["Object2"];
                output += $"{JsonSerializer.DeserializeString<Guid>(guid)}";

                this.TextBlockResult = $"{this.TextBlockResult}{output}{Environment.NewLine}";
                Console.WriteLine(output);
                Logger.Info("Message consumed successfully.");

                return true;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Handle message of object data type
        /// </summary>
        /// <param name="topicName">Topic name</param>
        /// <param name="messageType">Consumed message type</param>
        /// <param name="message">Consumed topic message</param>
        /// <param name="properties">Message meta-data/properties</param>
        /// <paramref name="publishUtcDT">Published UTC date time</param>
        /// <returns>Return TRUE if consumed successfully else FALSE</returns>
        private bool OnPersonReceived(TopicName topicName, GenericMessage message, DateTime publishUtcDT)
        {
            try
            {
                if (topicName == null)
                {
                    throw new ArgumentNullException(nameof(topicName));
                }

                //// Consume data type, object
                var originalMessage = JsonSerializer.Deserialize<Person>(message.Data);
                var output = $"Consumed message <Person>: {originalMessage}";

                this.TextBlockResult = $"{this.TextBlockResult}{output}{Environment.NewLine}";
                Console.WriteLine(output);
                Logger.Info("Message consumed successfully.");

                return true;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Handle message of CommandMessage type
        /// </summary>
        /// <param name="topicName">Topic name</param>
        /// <param name="commandMessage">Command message</param>
        /// <paramref name="publishUtcDT">Published UTC date time</param>
        /// <returns>Return TRUE if consumed successfully else FALSE</returns>
        private bool OnCommandReceived(TopicName topicName, CommandMessage commandMessage, DateTime publishUtcDT)
        {
            try
            {
                if (topicName == null)
                {
                    throw new ArgumentNullException(nameof(topicName));
                }
                else if (commandMessage == null)
                {
                    throw new ArgumentNullException(nameof(commandMessage));
                }

                //// Consume command message type
                var output = $"Consumed command message <CommandMessage>: {commandMessage}";

                this.TextBlockResult = $"{this.TextBlockResult}{output}{Environment.NewLine}";
                Console.WriteLine(output);
                Logger.Info("Message consumed successfully.");

                return true;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Handle message of EventMessage type
        /// </summary>
        /// <param name="topicName">Topic name</param>
        /// <param name="eventMessage">Event message</param>
        /// <paramref name="publishUtcDT">Published UTC date time</param>
        /// <returns>Return TRUE if consumed successfully else FALSE</returns>
        private bool OnEventReceived(TopicName topicName, EventMessage eventMessage, DateTime publishUtcDT)
        {
            try
            {
                if (topicName == null)
                {
                    throw new ArgumentNullException(nameof(topicName));
                }
                else if (eventMessage == null)
                {
                    throw new ArgumentNullException(nameof(eventMessage));
                }

                //// Consume event message type
                var output = $"Consumed event message <EventMessage>: {eventMessage}";

                this.TextBlockResult = $"{this.TextBlockResult}{output}{Environment.NewLine}";
                Console.WriteLine(output);
                Logger.Info("Message consumed successfully.");

                return true;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Error handler for subscriber.
        /// </summary>
        /// <param name="errorCode">Broker error code</param>
        /// <param name="errorReason">Error reason in clear text</param>
        /// <param name="isFatal">Flag indicating fatal error</param>
        private void OnSubscriberError(string errorCode, string errorReason, bool isFatal)
        {
            if (string.IsNullOrEmpty(errorCode))
            {
                Logger.Warn($"Unexpected subscriber error, no error code returned. Is fatal: {isFatal}");
            }
            else
            {
                Logger.Error($"Subscriber error: {errorCode}, Is fatal: {isFatal}");
            }
        }

        #endregion Subscribe Handlers


        #region Static Methods

        /// <summary>
        /// Generate random number
        /// </summary>
        /// <param name="min">Minimum value</param>
        /// <param name="max">Maximum value</param>
        /// <returns>Return random number</returns>
        private static int RandomNumber(int min = 0, int max = 100)
        {
            var random = new Random();
            return random.Next(min, max);
        }

        /// <summary>
        /// Generate a random string with a given size
        /// </summary>
        /// <param name="size">String length</param>
        /// <returns>Return a new string</returns>
        private static string RandomString(int size = 5)
        {
            var builder = new StringBuilder();
            var random = new Random();
            char ch;

            for (var i = 0; i < size; i++)
            {
                ch = Convert.ToChar(Convert.ToInt32(Math.Floor((26 * random.NextDouble()) + 65)));
                builder.Append(ch);
            }

            return builder.ToString();
        }

        #endregion Static Methods


        #region MVVM Property Changed Handler

        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(string info)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(info));
        }

        #endregion MVVM Property Changed Handler
    }
}
